/* router.cc
   Mathieu Stefani, 05 janvier 2016

   Rest routing implementation
*/

#include <algorithm>

#include <pistache/description.h>
#include <pistache/router.h>

namespace Pistache {
namespace Rest {

Request::Request(const Http::Request &request, std::vector<TypedParam> &&params,
                 std::vector<TypedParam> &&splats)
    : Http::Request(request), params_(std::move(params)),
      splats_(std::move(splats)) {}

bool Request::hasParam(const std::string &name) const {
  auto it = std::find_if(
      params_.begin(), params_.end(),
      [&](const TypedParam &param) { return param.name() == name; });

  return it != std::end(params_);
}

TypedParam Request::param(const std::string &name) const {
  auto it = std::find_if(
      params_.begin(), params_.end(),
      [&](const TypedParam &param) { return param.name() == name; });

  if (it == std::end(params_)) {
    throw std::runtime_error("Unknown parameter");
  }

  return *it;
}

TypedParam Request::splatAt(size_t index) const {
  if (index >= splats_.size()) {
    throw std::out_of_range("Request splat index out of range");
  }
  return splats_[index];
}

std::vector<TypedParam> Request::splat() const { return splats_; }

std::regex SegmentTreeNode::multiple_slash =
    std::regex("//+", std::regex_constants::optimize);

SegmentTreeNode::SegmentTreeNode()
    : resource_ref_(), fixed_(), param_(), optional_(), splat_(nullptr),
      route_(nullptr) {
  std::shared_ptr<char> ptr(new char[0], std::default_delete<char[]>());
  resource_ref_.swap(ptr);
}

SegmentTreeNode::SegmentTreeNode(const std::shared_ptr<char> &resourceReference)
    : resource_ref_(resourceReference), fixed_(), param_(), optional_(),
      splat_(nullptr), route_(nullptr) {}

SegmentTreeNode::SegmentType
SegmentTreeNode::getSegmentType(const std::string_view &fragment) {
  auto optpos = fragment.find('?');
  if (fragment[0] == ':') {
    if (optpos != std::string_view::npos) {
      if (optpos != fragment.length() - 1) {
        throw std::runtime_error("? should be at the end of the string");
      }
      return SegmentType::Optional;
    }
    return SegmentType::Param;
  } else if (fragment[0] == '*') {
    if (fragment.length() > 1) {
      throw std::runtime_error("Invalid splat parameter");
    }
    return SegmentType::Splat;
  }

  if (optpos != std::string_view::npos) {
    throw std::runtime_error(
        "Only optional parameters are currently supported");
  }

  return SegmentType::Fixed;
}

std::string SegmentTreeNode::sanitizeResource(const std::string &path) {
  const auto &dup = std::regex_replace(path, SegmentTreeNode::multiple_slash,
                                       std::string("/"));
  if (dup[dup.length() - 1] == '/') {
    return dup.substr(1, dup.length() - 2);
  }
  return dup.substr(1);
}

void SegmentTreeNode::addRoute(
    const std::string_view &path, const Route::Handler &handler,
    const std::shared_ptr<char> &resource_reference) {
  // recursion to correct path segment
  if (!path.empty()) {
    const auto segment_delimiter = path.find('/');
    // current segment value
    auto current_segment = path.substr(0, segment_delimiter);
    // complete child path (path without this segment)
    // if no '/' was found, it means that it is a leaf resource
    const auto lower_path = (segment_delimiter == std::string_view::npos)
                                ? std::string_view{nullptr, 0}
                                : path.substr(segment_delimiter + 1);

    std::unordered_map<std::string_view, std::shared_ptr<SegmentTreeNode>>
        *collection = nullptr;
    const auto fragmentType = getSegmentType(current_segment);
    switch (fragmentType) {
    case SegmentType::Fixed:
      collection = &fixed_;
      break;
    case SegmentType::Param:
      collection = &param_;
      break;
    case SegmentType::Optional:
      // remove the trailing question mark
      current_segment = current_segment.substr(0, current_segment.length() - 1);
      collection = &optional_;
      break;
    case SegmentType::Splat:
      if (splat_ == nullptr) {
        splat_ = std::make_shared<SegmentTreeNode>(resource_reference);
      }
      splat_->addRoute(lower_path, handler, resource_reference);
      return;
    }

    // if the segment tree nodes for the lower path does not exist
    if (collection->count(current_segment) == 0) {
      // first create it
      collection->insert(std::make_pair(
          current_segment,
          std::make_shared<SegmentTreeNode>(resource_reference)));
    }
    collection->at(current_segment)
        ->addRoute(lower_path, handler, resource_reference);
  } else { // current path segment requested
    if (route_ != nullptr)
      throw std::runtime_error("Requested route already exist.");
    route_ = std::make_shared<Route>(handler);
  }
}

bool Pistache::Rest::SegmentTreeNode::removeRoute(
    const std::string_view &path) {
  // recursion to correct path segment
  if (!path.empty()) {
    const auto segment_delimiter = path.find('/');
    // current segment value
    auto current_segment = path.substr(0, segment_delimiter);
    // complete child path (path without this segment)
    // if no '/' was found, it means that it is a leaf resource
    const auto lower_path = (segment_delimiter == std::string_view::npos)
                                ? std::string_view{nullptr, 0}
                                : path.substr(segment_delimiter + 1);

    std::unordered_map<std::string_view, std::shared_ptr<SegmentTreeNode>>
        *collection = nullptr;
    auto fragmentType = getSegmentType(current_segment);
    switch (fragmentType) {
    case SegmentType::Fixed:
      collection = &fixed_;
      break;
    case SegmentType::Param:
      collection = &param_;
      break;
    case SegmentType::Optional:
      // remove the trailing question mark
      current_segment = current_segment.substr(0, current_segment.length() - 1);
      collection = &optional_;
      break;
    case SegmentType::Splat:
      return splat_->removeRoute(lower_path);
    }

    try {
      const bool removable =
          collection->at(current_segment)->removeRoute(lower_path);
      if (removable) {
        collection->erase(current_segment);
      }
    } catch (const std::out_of_range &) {
      throw std::runtime_error("Requested does not exist.");
    }
  } else { // current leaf requested
    route_.reset();
  }
  return fixed_.empty() && param_.empty() && optional_.empty() &&
         splat_ == nullptr && route_ == nullptr;
}

std::tuple<std::shared_ptr<Route>, std::vector<TypedParam>,
           std::vector<TypedParam>>
Pistache::Rest::SegmentTreeNode::findRoute(
    const std::string_view &path, std::vector<TypedParam> &params,
    std::vector<TypedParam> &splats) const {
  // recursion to correct path segment
  if (!path.empty()) {
    const auto segment_delimiter = path.find('/');
    // current segment value
    auto current_segment = path.substr(0, segment_delimiter);
    // complete child path (path without this segment)
    // if no '/' was found, it means that it is a leaf resource
    const auto lower_path = (segment_delimiter == std::string_view::npos)
                                ? std::string_view{nullptr, 0}
                                : path.substr(segment_delimiter + 1);

    // Check if it is a fixed route
    if (fixed_.count(current_segment) != 0) {
      auto result =
          fixed_.at(current_segment)->findRoute(lower_path, params, splats);
      auto route = std::get<0>(result);
      if (route != nullptr)
        return result;
    }

    // Check if it is a path param
    for (const auto &param : param_) {
      std::string para_name{param.first.data(), param.first.length()};
      std::string para_val{current_segment.data(), current_segment.length()};
      params.emplace_back(para_name, para_val);
      auto result = param.second->findRoute(lower_path, params, splats);
      auto route = std::get<0>(result);
      if (route != nullptr)
        return result;
      params.pop_back();
    }

    // Check if it is an optional path param
    for (const auto &optional : optional_) {
      std::string opt_name{optional.first.data(), optional.first.length()};
      std::string opt_val{current_segment.data(), current_segment.length()};
      params.emplace_back(opt_name, opt_val);
      auto result = optional.second->findRoute(lower_path, params, splats);

      auto route = std::get<0>(result);
      if (route != nullptr)
        return result;
      params.pop_back();
      // try to find a route for lower path assuming that
      // this optional path param is not present
      result = optional.second->findRoute(lower_path, params, splats);

      route = std::get<0>(result);
      if (route != nullptr)
        return result;
    }

    // Check if it is a splat
    if (splat_ != nullptr) {
      std::string splat{current_segment.data(), current_segment.length()};
      splats.emplace_back(splat, splat);
      auto result = splat_->findRoute(lower_path, params, splats);

      auto route = std::get<0>(result);
      if (route != nullptr)
        return result;
      splats.pop_back();
    }
    // Requested route does not exists
    return std::make_tuple(nullptr, std::vector<TypedParam>(),
                           std::vector<TypedParam>());
  } else { // current leaf requested, or empty final optional
    if (!optional_.empty()) {
      // in case of more than one optional at this point, as it is an
      // ambiguity, it is resolved by using the first optional
      auto optional = optional_.begin();
      // std::string opt {optional->first.data(), optional->first.length()};
      return optional->second->findRoute(path, params, splats);
    } else if (route_ == nullptr) {
      // if we are here but route is null, we reached this point
      // trying to parse an optional, that was missing
      return std::make_tuple(nullptr, std::vector<TypedParam>(),
                             std::vector<TypedParam>());
    } else {
      return std::make_tuple(route_, std::move(params), std::move(splats));
    }
  }
}

std::tuple<std::shared_ptr<Route>, std::vector<TypedParam>,
           std::vector<TypedParam>>
Pistache::Rest::SegmentTreeNode::findRoute(const std::string_view &path) const {
  std::vector<TypedParam> params;
  std::vector<TypedParam> splats;
  return findRoute(path, params, splats);
}

namespace Private {

RouterHandler::RouterHandler(const Rest::Router &router)
    : router(std::make_shared<Rest::Router>(router)) {}

RouterHandler::RouterHandler(std::shared_ptr<Rest::Router> router)
    : router(std::move(router)) {}

void RouterHandler::onRequest(const Http::Request &req,
                              Http::ResponseWriter response) {
  auto resp = response.clone();
  router->route(req, std::move(resp));
}

} // namespace Private

Router Router::fromDescription(const Rest::Description &desc) {
  Router router;
  router.initFromDescription(desc);
  return router;
}

std::shared_ptr<Private::RouterHandler> Router::handler() const {
  return std::make_shared<Private::RouterHandler>(*this);
}

std::shared_ptr<Private::RouterHandler>
Router::handler(std::shared_ptr<Rest::Router> router) {
  return std::make_shared<Private::RouterHandler>(router);
}

void Router::initFromDescription(const Rest::Description &desc) {
  auto paths = desc.rawPaths();
  for (auto it = paths.flatBegin(), end = paths.flatEnd(); it != end; ++it) {
    const auto &paths_ = *it;
    for (const auto &path : paths_) {
      if (!path.isBound()) {
        std::ostringstream oss;
        oss << "Path '" << path.value << "' is not bound";
        throw std::runtime_error(oss.str());
      }

      addRoute(path.method, path.value, path.handler);
    }
  }
}

void Router::get(const std::string &resource, Route::Handler handler) {
  addRoute(Http::Method::Get, resource, std::move(handler));
}

void Router::post(const std::string &resource, Route::Handler handler) {
  addRoute(Http::Method::Post, resource, std::move(handler));
}

void Router::put(const std::string &resource, Route::Handler handler) {
  addRoute(Http::Method::Put, resource, std::move(handler));
}

void Router::patch(const std::string &resource, Route::Handler handler) {
  addRoute(Http::Method::Patch, resource, std::move(handler));
}

void Router::del(const std::string &resource, Route::Handler handler) {
  addRoute(Http::Method::Delete, resource, std::move(handler));
}

void Router::options(const std::string &resource, Route::Handler handler) {
  addRoute(Http::Method::Options, resource, std::move(handler));
}

void Router::removeRoute(Http::Method method, const std::string &resource) {
  if (resource.empty())
    throw std::runtime_error("Invalid zero-length URL.");
  auto &r = routes[method];
  const auto sanitized = SegmentTreeNode::sanitizeResource(resource);
  const std::string_view path{sanitized.data(), sanitized.size()};
  r.removeRoute(path);
}

void Router::head(const std::string &resource, Route::Handler handler) {
  addRoute(Http::Method::Head, resource, std::move(handler));
}

void Router::addCustomHandler(Route::Handler handler) {
  customHandlers.push_back(std::move(handler));
}

void Router::addMiddleware(Route::Middleware middleware) {
    middlewares.push_back(std::move(middleware));
}

void Router::addNotFoundHandler(Route::Handler handler) {
  notFoundHandler = std::move(handler);
}

void Router::invokeNotFoundHandler(const Http::Request &req,
                                   Http::ResponseWriter resp) const {
  notFoundHandler(Rest::Request(std::move(req), std::vector<TypedParam>(),
                                std::vector<TypedParam>()),
                  std::move(resp));
}

Route::Status Router::route(const Http::Request &http_req,
                            Http::ResponseWriter response) {
  const auto resource = http_req.resource();
  if (resource.empty())
    throw std::runtime_error("Invalid zero-length URL.");

  auto req = http_req;
  auto resp = response.clone();

  for (const auto &middleware : middlewares) {
    auto result = middleware(req, resp);

    // Handler returns true, go to the next piped handler, otherwise break and return
    if (! result)
      return Route::Status::Match;
  }

  auto &r = routes[req.method()];
  const auto sanitized = SegmentTreeNode::sanitizeResource(resource);
  const std::string_view path{sanitized.data(), sanitized.size()};
  auto result = r.findRoute(path);

  auto route = std::get<0>(result);
  if (route != nullptr) {
    auto params = std::get<1>(result);
    auto splats = std::get<2>(result);
    route->invokeHandler(Request(req, std::move(params), std::move(splats)),
                         std::move(resp));
    return Route::Status::Match;
  }

  for (const auto &handler : customHandlers) {
    auto resp = response.clone();
    auto handler1 = handler(
        Request(req, std::vector<TypedParam>(), std::vector<TypedParam>()),
        std::move(resp));
    if (handler1 == Route::Result::Ok)
      return Route::Status::Match;
  }

  // No route or custom handler found. Let's walk through the
  // list of other methods and see if any of them support
  // this resource.
  // This will allow server to send a
  // HTTP 405 (method not allowed) response.
  // RFC 7231 requires HTTP 405 responses to include a list of
  // supported methods for the requested resource.
  std::vector<Http::Method> supportedMethods;
  for (auto &methods : routes) {
    if (methods.first == req.method())
      continue;

    auto res = methods.second.findRoute(path);
    auto rte = std::get<0>(res);
    if (rte != nullptr) {
      supportedMethods.push_back(methods.first);
    }
  }

  if (!supportedMethods.empty()) {
    response.sendMethodNotAllowed(supportedMethods);
    return Route::Status::NotAllowed;
  }

  if (hasNotFoundHandler()) {
    invokeNotFoundHandler(req, std::move(response));
  } else {
    response.send(Http::Code::Not_Found, "Could not find a matching route");
  }
  return Route::Status::NotFound;
}

void Router::addRoute(Http::Method method, const std::string &resource,
                      Route::Handler handler) {
  if (resource.empty())
    throw std::runtime_error("Invalid zero-length URL.");
  auto &r = routes[method];
  const auto sanitized = SegmentTreeNode::sanitizeResource(resource);
  std::shared_ptr<char> ptr(new char[sanitized.length()],
                            std::default_delete<char[]>());
  memcpy(ptr.get(), sanitized.data(), sanitized.length());
  const std::string_view path{ptr.get(), sanitized.length()};
  r.addRoute(path, handler, ptr);
}

namespace Routes {

void Get(Router &router, const std::string &resource, Route::Handler handler) {
  router.get(resource, std::move(handler));
}

void Post(Router &router, const std::string &resource, Route::Handler handler) {
  router.post(resource, std::move(handler));
}

void Put(Router &router, const std::string &resource, Route::Handler handler) {
  router.put(resource, std::move(handler));
}

void Patch(Router &router, const std::string &resource,
           Route::Handler handler) {
  router.patch(resource, std::move(handler));
}

void Delete(Router &router, const std::string &resource,
            Route::Handler handler) {
  router.del(resource, std::move(handler));
}

void Options(Router &router, const std::string &resource,
             Route::Handler handler) {
  router.options(resource, std::move(handler));
}

void Remove(Router &router, Http::Method method, const std::string &resource) {
  router.removeRoute(method, resource);
}

void NotFound(Router &router, Route::Handler handler) {
  router.addNotFoundHandler(std::move(handler));
}

void Head(Router &router, const std::string &resource, Route::Handler handler) {
  router.head(resource, std::move(handler));
}

} // namespace Routes
} // namespace Rest
} // namespace Pistache
